module Hasura.RQL.DML.Internal
  ( SessionVariableBuilder,
    askDelPermInfo,
    askInsPermInfo,
    askPermInfo',
    askSelPermInfo,
    askUpdPermInfo,
    binRHSBuilder,
    checkPermOnCol,
    checkSelOnCol,
    convAnnBoolExpPartialSQL,
    convAnnColumnCaseBoolExpPartialSQL,
    convBoolExp,
    convPartialSQLExp,
    dmlTxErrorHandler,
    fetchRelDet,
    fetchRelTabInfo,
    fromCurrentSession,
    getPermInfoMaybe,
    getRolePermInfo,
    isTabUpdatable,
    onlyPositiveInt,
    runDMLP1T,
    sessVarFromCurrentSetting,
    validateHeaders,
    valueParserWithCollectableType,
    verifyAsrns,
    withTypeAnn,
  )
where

import Control.Lens
import Data.Aeson.Types
import Data.HashMap.Strict qualified as M
import Data.HashSet qualified as HS
import Data.Sequence qualified as DS
import Data.Text qualified as T
import Data.Text.Extended
import Database.PG.Query qualified as Q
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Error
import Hasura.Backends.Postgres.SQL.Types hiding (TableName)
import Hasura.Backends.Postgres.SQL.Value
import Hasura.Backends.Postgres.Translate.BoolExp
import Hasura.Backends.Postgres.Translate.Column
import Hasura.Backends.Postgres.Types.Column
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.Types
import Hasura.SQL.Types
import Hasura.Session

newtype DMLP1T m a = DMLP1T {unDMLP1T :: StateT (DS.Seq Q.PrepArg) m a}
  deriving
    ( Functor,
      Applicative,
      Monad,
      MonadTrans,
      MonadState (DS.Seq Q.PrepArg),
      MonadError e,
      SourceM,
      TableCoreInfoRM b,
      TableInfoRM b,
      CacheRM,
      UserInfoM,
      HasServerConfigCtx
    )

runDMLP1T :: DMLP1T m a -> m (a, DS.Seq Q.PrepArg)
runDMLP1T = flip runStateT DS.empty . unDMLP1T

mkAdminRolePermInfo :: Backend b => TableCoreInfo b -> RolePermInfo b
mkAdminRolePermInfo ti =
  RolePermInfo (Just i) (Just s) (Just u) (Just d)
  where
    fields = _tciFieldInfoMap ti
    pgCols = map pgiColumn $ getCols fields
    pgColsWithFilter = M.fromList $ map (,Nothing) pgCols
    scalarComputedFields =
      HS.fromList $ map _cfiName $ onlyScalarComputedFields $ getComputedFieldInfos fields
    scalarComputedFields' = HS.toMap scalarComputedFields $> Nothing

    tn = _tciName ti
    i = InsPermInfo (HS.fromList pgCols) annBoolExpTrue M.empty False mempty
    s = SelPermInfo pgColsWithFilter scalarComputedFields' annBoolExpTrue Nothing True mempty
    u = UpdPermInfo (HS.fromList pgCols) tn annBoolExpTrue Nothing M.empty mempty
    d = DelPermInfo tn annBoolExpTrue mempty

askPermInfo' ::
  (UserInfoM m, Backend b) =>
  PermAccessor b c ->
  TableInfo b ->
  m (Maybe c)
askPermInfo' pa tableInfo = do
  role <- askCurRole
  return $ getPermInfoMaybe role pa tableInfo

getPermInfoMaybe ::
  (Backend b) => RoleName -> PermAccessor b c -> TableInfo b -> Maybe c
getPermInfoMaybe role pa tableInfo =
  getRolePermInfo role tableInfo >>= (^. permAccToLens pa)

getRolePermInfo ::
  Backend b => RoleName -> TableInfo b -> Maybe (RolePermInfo b)
getRolePermInfo role tableInfo
  | role == adminRoleName =
    Just $ mkAdminRolePermInfo (_tiCoreInfo tableInfo)
  | otherwise =
    M.lookup role (_tiRolePermInfoMap tableInfo)

askPermInfo ::
  (UserInfoM m, QErrM m, Backend b) =>
  PermAccessor b c ->
  TableInfo b ->
  m c
askPermInfo pa tableInfo = do
  roleName <- askCurRole
  mPermInfo <- askPermInfo' pa tableInfo
  onNothing mPermInfo $
    throw400 PermissionDenied $
      mconcat
        [ pt <> " on " <>> tableInfoName tableInfo,
          " for role " <>> roleName,
          " is not allowed. "
        ]
  where
    pt = permTypeToCode $ permAccToType pa

isTabUpdatable :: RoleName -> TableInfo ('Postgres pgKind) -> Bool
isTabUpdatable role ti
  | role == adminRoleName = True
  | otherwise = isJust $ M.lookup role rpim >>= _permUpd
  where
    rpim = _tiRolePermInfoMap ti

askInsPermInfo ::
  (UserInfoM m, QErrM m, Backend b) =>
  TableInfo b ->
  m (InsPermInfo b)
askInsPermInfo = askPermInfo PAInsert

askSelPermInfo ::
  (UserInfoM m, QErrM m, Backend b) =>
  TableInfo b ->
  m (SelPermInfo b)
askSelPermInfo = askPermInfo PASelect

askUpdPermInfo ::
  (UserInfoM m, QErrM m, Backend b) =>
  TableInfo b ->
  m (UpdPermInfo b)
askUpdPermInfo = askPermInfo PAUpdate

askDelPermInfo ::
  (UserInfoM m, QErrM m, Backend b) =>
  TableInfo b ->
  m (DelPermInfo b)
askDelPermInfo = askPermInfo PADelete

verifyAsrns :: (MonadError QErr m) => [a -> m ()] -> [a] -> m ()
verifyAsrns preds xs = indexedForM_ xs $ \a -> mapM_ ($ a) preds

checkSelOnCol ::
  forall b m.
  (UserInfoM m, QErrM m, Backend b) =>
  SelPermInfo b ->
  Column b ->
  m ()
checkSelOnCol selPermInfo =
  checkPermOnCol @b PTSelect (HS.fromList $ M.keys $ spiCols @b selPermInfo)

checkPermOnCol ::
  forall b m.
  (UserInfoM m, QErrM m, Backend b) =>
  PermType ->
  HS.HashSet (Column b) ->
  Column b ->
  m ()
checkPermOnCol pt allowedCols col = do
  role <- askCurRole
  unless (HS.member col allowedCols) $
    throw400 PermissionDenied $ permErrMsg role
  where
    permErrMsg role
      | role == adminRoleName = "no such column exists : " <>> col
      | otherwise =
        mconcat
          [ "role " <>> role,
            " does not have permission to ",
            permTypeToCode pt <> " column " <>> col
          ]

checkSelectPermOnScalarComputedField ::
  forall b m.
  (UserInfoM m, QErrM m) =>
  SelPermInfo b ->
  ComputedFieldName ->
  m ()
checkSelectPermOnScalarComputedField selPermInfo computedField = do
  role <- askCurRole
  unless (M.member computedField $ spiScalarComputedFields selPermInfo) $
    throw400 PermissionDenied $ permErrMsg role
  where
    permErrMsg role
      | role == adminRoleName = "no such computed field exists : " <>> computedField
      | otherwise =
        "role " <> role <<> " does not have permission to select computed field" <>> computedField

valueParserWithCollectableType ::
  forall pgKind m.
  (Backend ('Postgres pgKind), MonadError QErr m) =>
  (ColumnType ('Postgres pgKind) -> Value -> m S.SQLExp) ->
  CollectableType (ColumnType ('Postgres pgKind)) ->
  Value ->
  m S.SQLExp
valueParserWithCollectableType valBldr pgType val = case pgType of
  CollectableTypeScalar ty -> valBldr ty val
  CollectableTypeArray ofTy -> do
    -- for arrays, we don't use the prepared builder
    vals <- runAesonParser parseJSON val
    scalarValues <- parseScalarValuesColumnType ofTy vals
    return $
      S.SETyAnn
        (S.SEArray $ map (toTxtValue . ColumnValue ofTy) scalarValues)
        (S.mkTypeAnn $ CollectableTypeArray (unsafePGColumnToBackend ofTy))

binRHSBuilder ::
  forall pgKind m.
  (Backend ('Postgres pgKind), QErrM m) =>
  ColumnType ('Postgres pgKind) ->
  Value ->
  DMLP1T m S.SQLExp
binRHSBuilder colType val = do
  preparedArgs <- get
  scalarValue <- parseScalarValueColumnType colType val
  put (preparedArgs DS.|> binEncoder scalarValue)
  return $ toPrepParam (DS.length preparedArgs + 1) (unsafePGColumnToBackend colType)

fetchRelTabInfo ::
  (QErrM m, TableInfoRM b m, Backend b) =>
  TableName b ->
  m (TableInfo b)
fetchRelTabInfo refTabName =
  -- Internal error
  modifyErrAndSet500 ("foreign " <>) $
    askTabInfoSource refTabName

data SessionVariableBuilder b m = SessionVariableBuilder
  { _svbCurrentSession :: !(SQLExpression b),
    _svbVariableParser :: !(SessionVarType b -> SessionVariable -> m (SQLExpression b))
  }

fetchRelDet ::
  (UserInfoM m, QErrM m, TableInfoRM b m, Backend b) =>
  RelName ->
  TableName b ->
  m (FieldInfoMap (FieldInfo b), SelPermInfo b)
fetchRelDet relName refTabName = do
  roleName <- askCurRole
  -- Internal error
  refTabInfo <- fetchRelTabInfo refTabName
  -- Get the correct constraint that applies to the given relationship
  refSelPerm <-
    modifyErr (relPermErr refTabName roleName) $
      askSelPermInfo refTabInfo

  return (_tciFieldInfoMap $ _tiCoreInfo refTabInfo, refSelPerm)
  where
    relPermErr rTable roleName _ =
      mconcat
        [ "role " <>> roleName,
          " does not have permission to read relationship " <>> relName,
          "; no permission on",
          " table " <>> rTable
        ]

checkOnColExp ::
  (UserInfoM m, QErrM m, TableInfoRM b m, Backend b) =>
  SelPermInfo b ->
  SessionVariableBuilder b m ->
  AnnBoolExpFldSQL b ->
  m (AnnBoolExpFldSQL b)
checkOnColExp spi sessVarBldr annFld = case annFld of
  AVColumn colInfo _ -> do
    let cn = pgiColumn colInfo
    checkSelOnCol spi cn
    return annFld
  AVRelationship relInfo nesAnn -> do
    relSPI <- snd <$> fetchRelDet (riName relInfo) (riRTable relInfo)
    modAnn <- checkSelPerm relSPI sessVarBldr nesAnn
    resolvedFltr <- convAnnBoolExpPartialSQL sessVarBldr $ spiFilter relSPI
    return $ AVRelationship relInfo $ andAnnBoolExps modAnn resolvedFltr
  AVComputedField cfBoolExp -> do
    roleName <- askCurRole
    let fieldName = _acfbName cfBoolExp
    case _acfbBoolExp cfBoolExp of
      CFBEScalar _ -> do
        checkSelectPermOnScalarComputedField spi fieldName
        pure annFld
      CFBETable table nesBoolExp -> do
        tableInfo <- modifyErrAndSet500 ("function " <>) $ askTabInfoSource table
        let errMsg _ =
              "role " <> roleName <<> " does not have permission to read "
                <> " computed field "
                <> fieldName <<> "; no permission on table " <>> table
        tableSPI <- modifyErr errMsg $ askSelPermInfo tableInfo
        modBoolExp <- checkSelPerm tableSPI sessVarBldr nesBoolExp
        resolvedFltr <- convAnnBoolExpPartialSQL sessVarBldr $ spiFilter tableSPI
        -- Including table permission filter; "input condition" AND "permission filter condition"
        let finalBoolExp = andAnnBoolExps modBoolExp resolvedFltr
        pure $ AVComputedField cfBoolExp {_acfbBoolExp = CFBETable table finalBoolExp}

convAnnBoolExpPartialSQL ::
  (Applicative f, Backend backend) =>
  SessionVariableBuilder backend f ->
  AnnBoolExpPartialSQL backend ->
  f (AnnBoolExpSQL backend)
convAnnBoolExpPartialSQL f =
  (traverse . traverse) (convPartialSQLExp f)

convAnnColumnCaseBoolExpPartialSQL ::
  (Applicative f, Backend backend) =>
  SessionVariableBuilder backend f ->
  AnnColumnCaseBoolExpPartialSQL backend ->
  f (AnnColumnCaseBoolExp backend (SQLExpression backend))
convAnnColumnCaseBoolExpPartialSQL f =
  (traverse . traverse) (convPartialSQLExp f)

convPartialSQLExp ::
  (Applicative f) =>
  SessionVariableBuilder backend f ->
  PartialSQLExp backend ->
  f (SQLExpression backend)
convPartialSQLExp sessVarBldr = \case
  PSESQLExp sqlExp -> pure sqlExp
  PSESession -> pure $ _svbCurrentSession sessVarBldr
  PSESessVar colTy sessionVariable -> (_svbVariableParser sessVarBldr) colTy sessionVariable

sessVarFromCurrentSetting ::
  (Applicative f) => SessionVariableBuilder ('Postgres pgKind) f
sessVarFromCurrentSetting =
  SessionVariableBuilder currentSession $ \ty var -> pure $ sessVarFromCurrentSetting' ty var

sessVarFromCurrentSetting' :: CollectableType PGScalarType -> SessionVariable -> S.SQLExp
sessVarFromCurrentSetting' ty sessVar =
  withTypeAnn ty $ fromCurrentSession currentSession sessVar

withTypeAnn :: CollectableType PGScalarType -> S.SQLExp -> S.SQLExp
withTypeAnn ty sessVarVal = flip S.SETyAnn (S.mkTypeAnn ty) $
  case ty of
    CollectableTypeScalar baseTy -> withConstructorFn baseTy sessVarVal
    CollectableTypeArray _ -> sessVarVal

fromCurrentSession ::
  S.SQLExp ->
  SessionVariable ->
  S.SQLExp
fromCurrentSession currentSessionExp sessVar =
  S.SEOpApp
    (S.SQLOp "->>")
    [currentSessionExp, S.SELit $ sessionVariableToText sessVar]

currentSession :: S.SQLExp
currentSession = S.SEUnsafe "current_setting('hasura.user')::json"

checkSelPerm ::
  (UserInfoM m, QErrM m, TableInfoRM b m, Backend b) =>
  SelPermInfo b ->
  SessionVariableBuilder b m ->
  AnnBoolExpSQL b ->
  m (AnnBoolExpSQL b)
checkSelPerm spi sessVarBldr =
  traverse (checkOnColExp spi sessVarBldr)

convBoolExp ::
  (UserInfoM m, QErrM m, TableInfoRM b m, BackendMetadata b) =>
  FieldInfoMap (FieldInfo b) ->
  SelPermInfo b ->
  BoolExp b ->
  SessionVariableBuilder b m ->
  TableName b ->
  ValueParser b m (SQLExpression b) ->
  m (AnnBoolExpSQL b)
convBoolExp cim spi be sessVarBldr rootTable rhsParser = do
  let boolExpRHSParser = BoolExpRHSParser rhsParser $ _svbCurrentSession sessVarBldr
  abe <- annBoolExp boolExpRHSParser rootTable cim $ unBoolExp be
  checkSelPerm spi sessVarBldr abe

dmlTxErrorHandler :: Q.PGTxErr -> QErr
dmlTxErrorHandler = mkTxErrorHandler $ \case
  PGIntegrityConstraintViolation _ -> True
  PGDataException _ -> True
  PGSyntaxErrorOrAccessRuleViolation (Just (PGErrorSpecific code)) ->
    code
      `elem` [ PGUndefinedObject,
               PGInvalidColumnReference
             ]
  _ -> False

-- validate headers
validateHeaders :: (UserInfoM m, QErrM m) => HashSet Text -> m ()
validateHeaders depHeaders = do
  headers <- getSessionVariables . _uiSession <$> askUserInfo
  forM_ depHeaders $ \hdr ->
    unless (hdr `elem` map T.toLower headers) $
      throw400 NotFound $ hdr <<> " header is expected but not found"

-- validate limit and offset int values
onlyPositiveInt :: MonadError QErr m => Int -> m ()
onlyPositiveInt i =
  when (i < 0) $
    throw400
      NotSupported
      "unexpected negative value"
